package br.ufrj.dcc.tp.servidor.model;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Queue;
import java.util.concurrent.ConcurrentHashMap;

import br.ufrj.dcc.tp.servidor.interfaces.IClient;
import br.ufrj.dcc.tp.servidor.interfaces.INotifier;
import br.ufrj.dcc.tp.servidor.log.Log;
import br.ufrj.dcc.tp.servidor.log.LogLevel;
import br.ufrj.dcc.tp.servidor.model.exception.ServerException;
import br.ufrj.dcc.tp.utils.Config;
import br.ufrj.dcc.tp.utils.SocketAction;
import br.ufrj.dcc.tp.utils.SocketMessageImpl;
import br.ufrj.dcc.tp.utils.interfaces.ISocketMessage;

public class Notifier extends Thread implements INotifier {
	
	/** Lista de clientes conectados. */
	private ConcurrentHashMap<String, IClient> clients;
	/** Fila de mensagens a serem enviadas. */
	private Queue<ISocketMessage> buffer;
	
	public Notifier() {
		clients = new ConcurrentHashMap<String, IClient>();
		buffer = new LinkedList<ISocketMessage>();
	}
	
	@Override
	public void subscribe(IClient client) throws ServerException {
		String nome = client.getNome();
		if( clients.containsKey(nome)) {
			throw new ServerException("Cliente duplicado");
		}
		clients.put(nome, client);
	}

	@Override
	public void unsubscribe(IClient client, boolean notify) throws ServerException, IOException {
		String nome = client.getNome();
		if( ! clients.containsKey(nome) ) {
			return;
		}
		clients.remove(nome);
		if( notify ) {
			notifySubscriptionsChange(client, "Desconectou");
		}
	}

	@Override
	public void update(ISocketMessage message) {
		synchronized (buffer) {
			buffer.offer(message);
		}
	}
	
	@Override
	public void unsubscribeAll() {
		if(clients.size() == 0)
			return;
		for(IClient client : clients.values() ) {
			try {
				unsubscribe(client, false);
				client.send(
						new SocketMessageImpl(Server.SERVER_NAME, SocketAction.SEND_MESSAGE, "Servidor desconectou...")
				);
				client.disconnect();
			} catch (IOException e) {
				Log.write(this.getClass(), LogLevel.SEVERE, "Erro ao desconectar cliente " + client.getNome() + "." );
				Log.write(this.getClass(), e);
			} catch (ServerException e) {
				Log.write(this.getClass(), LogLevel.SEVERE, "Erro ao desconectar cliente " + client.getNome() + "." );
				Log.write(this.getClass(), e);
			}
		}
	}
	
	/**
	 * Recebe a mensagem do cliente e repassa para todos os destinatários.
	 * @param message
	 */
	private void _update(ISocketMessage message) {
		if( message.getRecipients() != null ) {
			// Formata a mensagem para aparecer na tela do destinatario
			message = formatSendMessage(message);
			List<String> recipients = message.getRecipients();
			recipients.add(message.getSender());
			
			for( String nome : recipients ) {
				IClient client = clients.get(nome);
				if( client == null ) {
					Log.write(this.getClass(), LogLevel.WARNING, "Cliente " + nome + " nao encontrado.");
				} else {
					// Se ele ja terminou de conectar...
					try {
						client.send(message);
						Log.write(this.getClass(), LogLevel.INFO, "Mensagem enviada para " + nome + ".");
					} catch (IOException e) {
						Log.write(this.getClass(), LogLevel.SEVERE, "Mensagem nao pode ser enviada para " + nome + ".");
						Log.write(this.getClass(), e);
					}
				}
			}
		} else {
			_updateAll(message);
		}
	}
	
	private void _updateAll(ISocketMessage message) {
		// Formata a mensagem para aparecer na tela do destinatario
		message = formatSendMessage(message);
		for( IClient client : clients.values() ) {
			try {
				client.send(message);
				Log.write(this.getClass(), LogLevel.INFO, "Mensagem de " + message.getAction() + " enviada para " + client.getNome() + ".");
			} catch (IOException e) {
				Log.write(this.getClass(), LogLevel.SEVERE, "Mensagem nao pode ser enviada para " + client.getNome() + ".");
				Log.write(this.getClass(), e);
			}
		}
	}

	@Override
	public void run() {
		try {
			while(true) {
				synchronized (buffer) {
					try {
						if( buffer.size() > 0 ) {
							ISocketMessage message = buffer.remove();
							_update(message);
						}
					} catch(NoSuchElementException e) {}
				}
				Thread.sleep(Config.SLEEP_TIMEOUT.toInteger());
			}
		} catch( InterruptedException e ) {
		}
	}

	@Override
	public IClient getSubscribe(String nome) {
		return clients.get(nome);
	}
	
	@Override
	public List<String> getSubscriptions() {
		return buildSubscriptions(clients.keySet());
	}
	
	private List<String> buildSubscriptions(Collection<String> subs) {
		List<String> subscriptions = new ArrayList<String>(subs.size());
		
		for( String subscription : subs ) {
			subscriptions.add(subscription);
		}
		
		if( subscriptions.size() == 0 ) {
			subscriptions = null;
		}
		return subscriptions;
	}
	
	
	private ISocketMessage formatSendMessage(ISocketMessage message) {
		if( message instanceof SocketMessageImpl ) {
			
			if( message.getBody() != null && message.getBody().getClass().equals(String.class) ) {
				SocketMessageImpl newMessage = new SocketMessageImpl();
				
				newMessage.setAction(message.getAction())
					.setRecipients(message.getRecipients())
					.setSender(message.getSender());
				
				// Formata a mensagem para: NOME > MENSAGEM
				StringBuffer textMessage = new StringBuffer(message.getSender());
				textMessage.append(" => [");
				
				if( newMessage.getRecipients() == null ) {
					textMessage.append("all]");
				} else {
					List<String> recipients = newMessage.getRecipients();
					int size = recipients.size();
					for( int i = 0; i < size; ++i ) {
						textMessage.append(recipients.get(i));
						if( i+1 < size ) {
							textMessage.append(", ");
						}
					}
					textMessage.append("]");
					
				}
				textMessage.append(": ")
				.append(message.getBody().toString());
				
				newMessage.setMessage(textMessage.toString());
				
				return newMessage;
			}
		}
		
		return message;
	}

	@Override
	public void notifySubscriptionsChange(IClient client, String eventName) {
		_updateAll(
			new SocketMessageImpl(Server.SERVER_NAME, SocketAction.LIST_USERS, getSubscriptions())
		);
		
		if( eventName != null ) {
			_updateAll(
				new SocketMessageImpl(Server.SERVER_NAME, SocketAction.SEND_MESSAGE,
					client.getNome().concat(" ").concat(eventName).concat("."))
			);
		}
	}
	
	@Override
	public int getNumClients() {
		return clients.size();
	}
}
